Java, C, C++, Kotlin에서의 얕은 복사(Shallow Copy) vs 깊은 복사(Deep Copy) 가이드
객체나 참조형 자료구조를 다룰 때 얕은 복사와 깊은 복사의 차이를 이해하는 것은 버그를 방지하고 의도한 동작을 구현하는 데 매우 중요합니다. 아래는 해당 개념과 각 언어(Java, C, C++, Kotlin)에서의 자세한 설명과 예제를 포함한 비교입니다.
1. 정의
-
▶️ 얕은 복사 (Shallow Copy):
객체의 최상위 필드 값 또는 참조만 복사함. 만약 객체가 다른 객체를 참조하고 있다면, 내부 객체는 복사하지 않고 참조만 복사되므로 원본과 복사본이 내부 데이터를 공유합니다. -
🧬 깊은 복사 (Deep Copy):
객체 자신뿐만 아니라 그 객체가 참조하고 있는 내부 객체까지 완전히 새로운 객체로 재귀적으로 복사함. 복사본과 원본은 전혀 별개의 구조임.
2. Java
👉 얕은 복사
Object.clone()이나 복사 생성자 사용clone()의 기본 구현(super.clone())은 얕은 복사만 수행
예제:
class Address {
String city;
Address(String city) { this.city = city; }
}
class Person implements Cloneable {
String name;
Address address;
public Person(String name, Address address) { this.name = name; this.address = address; }
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 얕은 복사
}
}
Person p1 = new Person("Alice", new Address("Seoul"));
Person p2 = (Person) p1.clone();
p1.address.city = "Busan";
System.out.println(p2.address.city); // 출력: Busan (p1과 p2가 같은 Address를 참조)
✅ 깊은 복사
clone()을 오버라이드하여 내부 객체들도 별도로 복사
class Person implements Cloneable {
String name;
Address address;
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = new Address(this.address.city); // 깊은 복사
return cloned;
}
}
3. C
👉 얕은 복사
- 구조체 대입 연산(=)은 필드를 복사하지만, 포인터는 값만 복사하므로 같은 객체를 가리킴
typedef struct {
char *city;
} Address;
typedef struct {
char *name;
Address *address;
} Person;
Person p1 = { "Alice", malloc(sizeof(Address)) };
p1.address->city = strdup("Seoul");
Person p2 = p1; // 얕은 복사
strcpy(p1.address->city, "Busan");
printf("%s\n", p2.address->city); // 출력: Busan (동일한 주소 참조)
✅ 깊은 복사
- 내부 포인터들도 새로운 메모리를 할당한 후 내용 복사
Person deepCopyPerson(const Person *src) {
Person dest;
dest.name = strdup(src->name);
dest.address = malloc(sizeof(Address));
dest.address->city = strdup(src->address->city);
return dest;
}
4. C++
👉 얕은 복사
- 기본 복사 생성자/대입 연산자는 멤버 단위 복사만 수행됨 (포인터는 얕은 복사)
struct Address {
std::string city;
};
struct Person {
std::string name;
Address *address;
};
Person p1 = {"Alice", new Address{"Seoul"}};
Person p2 = p1; // 얕은 복사
p1.address->city = "Busan";
std::cout << p2.address->city << std::endl; // 출력: Busan
✅ 깊은 복사
- 사용자 정의 복사 생성자 및 대입 연산자를 구현해야 함
struct Person {
std::string name;
Address *address;
Person(const Person& other) {
name = other.name;
address = new Address(*other.address); // 깊은 복사
}
Person& operator=(const Person& other) {
if (this != &other) {
delete address;
address = new Address(*other.address);
name = other.name;
}
return *this;
}
};
5. Kotlin
👉 얕은 복사
data class의copy()는 얕은 복사만 수행 (내부 참조는 공유됨)
data class Address(var city: String)
data class Person(val name: String, val address: Address)
val p1 = Person("Alice", Address("Seoul"))
val p2 = p1.copy()
p1.address.city = "Busan"
println(p2.address.city) // 출력: Busan (같은 Address 인스턴스를 참조)
✅ 깊은 복사
copy()를 오버라이드하거나 수동으로 중첩 객체도 복사
fun Person.deepCopy() = Person(name, Address(address.city))
val p2 = p1.deepCopy()
p1.address.city = "Busan"
println(p2.address.city) // 출력: Seoul
✅ 요약 비교 표
| 항목 | 얕은 복사 (Shallow Copy) | 깊은 복사 (Deep Copy) |
|---|---|---|
| 복사 대상 | 최상위 필드 복사, 참조는 그대로 | 모든 필드 및 참조 객체까지 깊숙이 재복사 |
| 내부 객체 공유 | 예 (참조 공유) | 아니오 (독립된 복사본) |
| 구현 방법 | 기본 복사 생성자, 대입 연산자, clone(), copy() | 재귀적 복사 또는 수동 구현 필요 |
| 주의사항 | 변경 시 원본/복사본 모두 영향을 미침 | 구현이 복잡할 수 있고 성능에 부담 있음 |
💡 실전 팁
- 얕은 복사는 빠르지만, 객체 내부 상태가 공유될 경우 의도치 않은 변경이 생기는 문제에 주의해야 합니다.
- 깊은 복사는 신뢰할 수 있는 분리 복사이지만 성능/메모리 비용이 존재합니다.
- Java/C++/Kotlin: 참조형 필드는 clone() 또는 수동 복사 필요
- C: 포인터는 항상 새 메모리에 복사해야 진정한 깊은 복사입니다.
- 기본 타입은 샐로우/딥 복사에 상관 없이 값 복사로 동일하게 처리됨